package org.juiceshop.gutil;

import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.codehaus.groovy.runtime.InvokerInvocationException;

import groovy.lang.GroovyObject;
import groovy.lang.GroovySystem;
import groovy.lang.MetaClass;

public class DynamicObject<T> implements GroovyObject {
	

	public static <X> DynamicObject<X> create(X delegate) {
		return castToDynamicObject(new DynamicObject<X>(delegate,null));
	}
	
	public static <X> DynamicObject<X> create(Class<X> type) {
		return castToDynamicObject(new DynamicObject<X>(null, type));
	}
	
	public static <X,Y> IterableDynamicObject<X,Y> create(X delegate, Class<Y> elementType) {
		return new IterableDynamicObject<X,Y>(delegate,elementType);
	}
	
	public static <X extends Iterable<Y>,Y> IterableDynamicObject<X,Y> create(X delegate, Class<Y> elementType) {
		return new IterableDynamicObject<X,Y>(delegate,elementType);
	}
	

	protected T _object;
	protected Class<T> _type;
	protected GroovyObject _groovyObject;
	protected MetaClass _metaClass;
	protected boolean _groovy;
	protected boolean _static;
	
	private static final Object[] NOARGS = new Object[0];

		
	protected DynamicObject (T delegate, Class<T> type) {
		_object = delegate;
		_type = type;
		if (_type==null) {
			_type = castToClass(delegate.getClass());
		}
		if (delegate!=null && delegate instanceof GroovyObject) {
			_groovyObject = (GroovyObject)delegate;
			_metaClass = _groovyObject.getMetaClass();
			_groovy = true;
		} else {
			_metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(_object.getClass());
			_groovy = false;
		}
		_groovy = GroovyObject.class.isAssignableFrom(_type);
		_static = _object==null;
	}
	
	public T getObject() {
		return _object;
	}
	
	public boolean isGroovy() {
		return _groovy;
	}
	
	@Override
	public MetaClass getMetaClass() {
		return _metaClass;
	}

	@Override
	public Object getProperty(String propertyName) {
		try {
			if (_groovy && !_static) {
				return _groovyObject.getProperty(propertyName);
			} else {
				return _metaClass.getProperty(_object, propertyName);
			}
		} catch (InvokerInvocationException ex) {
			Throwable cause = ex.getCause();
			if (cause instanceof RuntimeException) {
				throw (RuntimeException) cause;
			}
			throw ex;
		}
	}

	@Override
	public void setProperty(String propertyName, Object newValue) {
		try {
			if (_groovy && !_static) {
				_groovyObject.setProperty(propertyName, newValue);
			} else {
				_metaClass.setProperty(_object, propertyName, newValue);
			}
		} catch (InvokerInvocationException ex) {
			Throwable cause = ex.getCause();
			if (cause instanceof RuntimeException) {
				throw (RuntimeException) cause;
			}
			throw ex;
		}
	}
	
	@Override
	public Object invokeMethod(String name, Object args) {
		try {
			if (_groovy && !_static) {
				return _groovyObject.invokeMethod(name, args);
			} else {
				return _metaClass.invokeMethod(_object, name, args);
			}			
		} catch (InvokerInvocationException ex) {
			Throwable cause = ex.getCause();
			if (cause instanceof RuntimeException) {
				throw (RuntimeException) cause;
			}
			throw ex;
		}
	}

	@Override
	public void setMetaClass(MetaClass metaClass) {
		if (_groovy && !_static) {
			_groovyObject.setMetaClass(metaClass);
			_metaClass = _groovyObject.getMetaClass();
		} else {
			GroovySystem.getMetaClassRegistry().setMetaClass(_type, metaClass);
			_metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(_type);
		}
	}
	
	public boolean hasProperty (String property) {
		return _metaClass.hasProperty(_object, property)!=null;
	}
	
	public boolean respondsTo (String method, Object... args) {
		List<?> metaMethods =_metaClass.respondsTo(_object, method);
//		if (metaMethods!=null && metaMethods.size()>1) {
//			throw new IllegalArgumentException("More than one method with signature "+method+args);
//		}
		return metaMethods!=null && metaMethods.size()>0;
	}
	
	//////////////////////////////////////////////////////////////////
	// Convenience methods
	//////////////////////////////////////////////////////////////////
	
	
	public <U> U get (Class<U> type, Object property) {
		Object[] args = new Object[]{property};
		Object result;
		if (respondsTo("get",args)) {
			result = invokeMethod("get", args);
		} else if (! (property instanceof String)) {
			throw new IllegalArgumentException("Property name must be as String but is "+property);
		} else {
			result = getProperty((String)property);
		}
		return type.cast(result);
	}
	
	public Object get (Object property) {
		return get(Object.class,property);
	}
	
	public void set (Object property, Object value) {
		Object[] args = new Object[]{property,value};
		if (respondsTo("set",args)) {
			invokeMethod("set",args);
		} else if (! (property instanceof String)) {
			throw new IllegalArgumentException("Property name must be as String but is "+property);
		} else {
			setProperty((String)property,value);
		}
	}
		
	public <U> U call (Class<U> type, String method, Object...args) {
		Object result = invokeMethod(method, args);
		return type.cast(result);
	}
	
	public Object call (String method, Object...args) {
		return call(Object.class,method,args);
	}
	
	//////////////////////////////////////////////////////////////////
	// GDK methods
	//////////////////////////////////////////////////////////////////
	
	public String dump() {
		return call(String.class,"dump");
	}
	
	public Map<String,Object> getProperties() {
		Object result = call("getProperties");
		return castToMap(result);
	}
	
	public String inspect() {
		return call(String.class,"inspect");
	}
	
	
	public <E> Iterator<E> iterator(Class<E> elementType) {
		Object obj = invokeMethod("iterator",NOARGS);
		if (obj==null) return null;
		return castToIterator(obj);
	}
	
	@Override
	public String toString() {
		return call(String.class,"toString");
	}
	
	
	public T newInstance(Object...args) {
		Object obj = _metaClass.invokeConstructor(args);
		return _type.cast(obj);
	}
	
	public DynamicObject<T> newDynamicInstance(Object...args) {
		return create(newInstance(args));
	}
	
	//////////////////////////////////////////////////////////////////
	// Static variants of GDK methods
	//////////////////////////////////////////////////////////////////

	public static MetaClass getMetaClass(Object object) {
		if (object instanceof GroovyObject) {
			return ((GroovyObject)object).getMetaClass();
		} else {
			return getMetaClass(object.getClass());
		}
	}
	
	public static MetaClass getMetaClass(Class<?> type) {
		return GroovySystem.getMetaClassRegistry().getMetaClass(type);
	}
	
	public static <X> X newInstance(Class<X> type, Object...args) {
		Object result = getMetaClass(type).invokeConstructor(args);
		return type.cast(result);
	}
	
	
	//////////////////////////////////////////////////////////////////
	// Helper methods to prevent unchecked classcast warnings.
	//////////////////////////////////////////////////////////////////
	
	/**
	 * Unchecked cast to {@link Iterator}.
	 */
	@SuppressWarnings("unchecked")
	protected static <E> Iterator<E> castToIterator(Object obj) {
		return Iterator.class.cast(obj);
	}
	
	/**
	 * Unchecked cast to {@link Map}.
	 * @param <E>
	 * @param <F>
	 * @param obj
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected static <E,F> Map<E,F> castToMap(Object obj) {
		return Map.class.cast(obj);
	}
	
	@SuppressWarnings("unchecked")
	protected static <D> DynamicObject<D> castToDynamicObject(Object obj) {
		return DynamicObject.class.cast(obj);
	}
	
	@SuppressWarnings("unchecked")
	protected static <E> Class<E> castToClass(Object obj) {
		return Class.class.cast(obj);
	}

	//////////////////////////////////////////////////////////////////
	// Other helper methods.
	//////////////////////////////////////////////////////////////////
	
}
